Hexo-基于 CryptoJS-AES 的 Hexo 文章片段加密插件

小小怪说 AES 算法很安全!

看一看

耍一耍

CryptoJS-AES 加密/解密工具

私钥:

公钥:

内容:

写一写

hexo-blog-encrypt 这个插件只可以加密整个文章,却不能只加密文章中的某个片段,不够灵活。借助 CryptoJS-AES 开发一个简易的文章片段加密插件!这样就可以在我的日记中方便地隐藏我不想公开的部分(比如说 pro 哥坏话)!

前端

研究一下 crypto-js 中 AES 的使用。我想让我的文章在前端中只暴露公钥,不暴露密码和内容。目前 AES 还不能被破解(小小怪如此说道),因此在只有公钥的情况下,不输入正确的密码是无法看到内容的:

{% AES '123','密码是"123"~' %} Hello world! {% endAES%}


在网页中引入 crypto-js

html
<script src="crypto-js.min.js" defer></script>

借助 crypto-js,可以写一个使用 AES 加密的逻辑:

js
CryptoJS.AES.encrypt(内容, 私钥).toString()

CryptoJS 默认使用 AES-128 加密算法。如果需要使用 AES-256 加密算法,可以在调用 encrypt 方法时传入自定义的秘钥和参数。(但我觉得 AES-128 算法就够用了)

这个函数输入:

  • 内容:<p>Hello world!<p>
  • 私钥:123

将会输出:

  • 公钥:U2FsdGVkX1/sbg7iJYyl2slEm9bWPVigmMvoutWnbR+031yQYZcWS2tBoaJMRtbY

每次输出的公钥还不唯一……

使用 AES 解密:

js
CryptoJS.AES.decrypt(公钥, 私钥).toString(CryptoJS.enc.Utf8));

如果公钥和私钥正确,将会正确返回内容,如果错误则返回空。

有时候解密时会出现 Error: Malformed UTF-8 data 错误导致解密失败,参考一下 javascript - Why I get Malformed UTF-8 data error on crypto-js? - Stack Overflow……我也不知道具体啥情况,多检查几遍代码看看哪里有错吧……


好的,现在我们正确掌握了内容、公钥和私钥之间的转换逻辑。如果内容的字符串是一段 HTML 代码,则我们可以将其渲染出来

基于此,我们可以使用 JQuery 定义一个 AESContainer 类以实现我们的文章加密插件:

javascript
class AESContainer {
    constructor(label, pubkey) {
        this.pubkey = pubkey;
        this.container = $('<div>').addClass('AES-container');
        this.inputContainer = $('<div>').addClass('AES-input');
        this.inputField = $('<input>').attr({type: 'password', required: true});
        this.highlight = $('<span>').addClass('hl');
        this.bar = $('<span>').addClass('bar');
        this.label = $('<label>').text(label);
 
        this.inputContainer.append(this.inputField, this.highlight, this.bar, this.label);
        this.container.append(this.inputContainer);
 
        this.inputField.on('keypress', this.handleKeyPress.bind(this));
    }
 
    handleKeyPress(event) {
        if (event.key === 'Enter') {
            this.decrypted = CryptoJS.AES.decrypt(this.pubkey, this.inputField.val()).toString(CryptoJS.enc.Utf8);
            if (this.decrypted) {
                this.inputContainer.remove();
                this.container.append($(this.decrypted));
            }
        }
    }
 
    render() {
        $(document.currentScript).before(this.container);
    }
}

设计偷一个好看的 CSS:

css
.AES-container {
    border: 2px solid var(--border);
    margin: 10px auto;
    padding: 10px 20px;
    width: 100%;
    box-sizing: border-box;
    transition: border 0.5s ease-in-out;
}
 
/* form starting stylings ------------------------------- */
.AES-container .AES-input {
    position: relative;
    margin: 20px 0 10px;
    box-sizing: border-box;
}
 
.AES-input input {
    font-size: 16px;
    padding: 5px 2px;
    display: block;
    width: calc(100% - 4px);
    border: none;
    border-bottom: 2px solid var(--border);
    background: none;
    color: var(--text-primary);
    transition: color 0.5s ease-in-out, border 0.5s ease-in-out;
}
 
.AES-input input:focus {
    outline: none;
}
 
/* LABEL ======================================= */
.AES-input label {
    color: var(--text-secondary);
    font-size: 16px;
    font-weight: normal;
    position: absolute;
    pointer-events: none;
    top: -5px;
    transition: 0.2s ease all;
    -moz-transition: 0.2s ease all;
    -webkit-transition: 0.2s ease all;
}
 
/* active state */
.AES-input input:focus~label,
.AES-input input:valid~label {
    top: -20px;
    font-size: 14px;
    color: var(--text-link);
}
 
/* BOTTOM BARS ================================= */
.AES-input .bar {
    position: relative;
    display: block;
    width: 100%;
}
 
.AES-input .bar:before,
.AES-input .bar:after {
    content: '';
    height: 2px;
    width: 0;
    transform: translateY(-2px);
    position: absolute;
    background: var(--text-link);
    transition: 0.2s ease all;
    -moz-transition: 0.2s ease all;
    -webkit-transition: 0.2s ease all;
}
 
.AES-input .bar:before {
    left: 50%;
}
 
.AES-input .bar:after {
    right: 50%;
}
 
/* active state */
.AES-input input:focus~.bar:before,
.AES-input input:focus~.bar:after {
    width: 50%;
}
 
/* hlER ================================== */
.AES-input .hl {
    position: absolute;
    height: 60%;
    width: 100px;
    top: 25%;
    left: 0;
    pointer-events: none;
    opacity: 0.5;
}
 
/* active state */
.AES-input input:focus~.hl {
    -webkit-animation: inputhler 0.3s ease;
    -moz-animation: inputhler 0.3s ease;
    animation: inputhler 0.3s ease;
}
 
/* ANIMATIONS ================ */
@-webkit-keyframes inputhler {
    from {
        background: var(--text-link);
    }
 
    to {
        width: 0;
        background: transparent;
    }
}
 
@-moz-keyframes inputhler {
    from {
        background: var(--text-link);
    }
 
    to {
        width: 0;
        background: transparent;
    }
}
 
@keyframes inputhler {
    from {
        background: var(--text-link);
    }
 
    to {
        width: 0;
        background: transparent;
    }
}

如此做,使用如下语句:

html
<script>
    new AESContainer('标签提示词', '公钥').render();
</script>

即可在 <script> 前创建并渲染一个输入密码的提示框:

html
<div class="AES-container">
    <div class="AES-input">
        <input type="password" required="required">
        <span class="hl"></span>
        <span class="bar"></span>
        <label>标签提示词</label>
    </div>
</div>

真是太棒了!现在我们需要后端帮我们自动生成公钥。

后端

借助 标签插件(Tag)| Hexo,在 Markdown 中如此写作:

markdown
{% AES '123','密码是"123"~' %}
Hello world!
{% endAES %}

经过 Hexo 渲染后转义为:

html
<script>new AESContainer('密码是"123"~', 'U2FsdGVkX1+LNox3Pwx7PH6x6yoSjddDb1gcOrYcFddHTHX/6AEXT0VTZUI1nhN5').render();</script>

便大功告成!


在 Hexo 项目根目录下输入如下命令以安装 crypto-js

shell
npm install crypto-js

在 Hexo 项目下的 scripts 文件夹下新建文件 AES.js,里面写转义标签的逻辑:

javascript
'use strict'
 
var CryptoJS = require("crypto-js");
 
const parseArgs = args => {
    return args.join(' ').split(',')
}
 
const AESFn = (args, content) => {
    const [password = "", label = '这里的内容需要输入密码才能查看~'] = parseArgs(args)
    content = hexo.render.renderSync({ text: content, engine: 'markdown' });
    if (password == "") {
        return content;
    } else {
        const pubkey = CryptoJS.AES.encrypt(content, password).toString();
        const result = `<script>new AESContainer('${label}', '${pubkey}').render();</script>`;
        return result;
    }
}
 
hexo.extend.tag.register('AES', AESFn, { ends: true })
  • const [password = "", label = '这里的内容需要输入密码才能查看~'] = parseArgs(args) 获取参数,对于标签:

    markdown
    {% AES '123','密码是"123"~' %}
    • password 为设定的密码 123
    • label 为标签提示词 密码是"123"~,如果为空,则默认值:这里的内容需要输入密码才能查看~,注意逗号后不要有空格。
  • hexo.extend.tag.register('AES', AESFn, { ends: true }) 对于所有 {% AES %}{% endAES %} 标签,调用 ASEFn() 函数处理。

  • hexo.render.renderSync({ text: content, engine: 'markdown' }); 借助 Hexo 渲染引擎,将 Markdown 语句转义为 HTML 语句。

  • CryptoJS.AES.encrypt(content, password).toString(); 该函数输入私钥和内容,输出成公钥。

  • <script>new AESContainer('${label}', '${pubkey}').render();</script> 渲染最后转义出的内容并置于当前 <script> 前。

重新编译,大功告成。

试一试

念桥边红药

在下面的框框中输入密码 promefire 即可产生 promefire 最喜欢的诗句!

markdown
{% AES 'promefire','密码是"promefire"~' %}
<marquee behavior="scroll" direction="right" scrollamount="15"><font color="red" size="4px">念桥边红药,年年知为谁生?</font></marquee>
{% endAES %}

{% AES 'promefire','密码是"promefire"~' %} 念桥边红药,年年知为谁生? {% endAES %}

大故宫

在下面的框框中输入密码 12345678 即可欣赏大故宫!

markdown
{% AES '12345678','密码是"12345678"~' %}
![人去楼不空 往昔的叱咤化作春色满园](/posts/Diary-老儿北儿京儿/1435.webp)
<center>人去楼不空 往昔的叱咤化作春色满园</center>
{% endAES %}

请注意:解密后的显示的内容可能需要重新调用一下网页初始化的相关函数才能达到未加密的显示效果!

{% AES '12345678','密码是"12345678"~' %} 人去楼不空 往昔的叱咤化作春色满园

人去楼不空 往昔的叱咤化作春色满园
{% endAES %}